package me.lwn.auth.security.provisioning;

import me.lwn.auth.resources.cmd.ResetPassword;
import me.lwn.auth.security.core.userdetails.User;
import org.springframework.core.log.LogMessage;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserCache;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.userdetails.cache.NullUserCache;
import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl;
import org.springframework.security.provisioning.GroupManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.util.Assert;

import javax.sql.DataSource;
import java.util.*;

import static me.lwn.auth.security.utils.UserDetailsSqlConstants.*;
import static org.springframework.security.provisioning.JdbcUserDetailsManager.*;

/**
 * TODO 此类暂时如此，不是最佳实践，后续可能进行调整
 */
public class CustomizeJdbcUserDetailsManager extends JdbcDaoImpl implements UserDetailsManager, GroupManager {

    private final String createUserSql = CREATE_USER_SQL;
    private final String changePasswordSql = CHANGE_PASSWORD_SQL;
    private final String createAuthoritySql = DEF_INSERT_AUTHORITY_SQL;
    private final String deleteUserAuthoritiesSql = DEF_DELETE_USER_AUTHORITIES_SQL;
    private String updateUserSql = UPDATE_USER_SQL;
    private String userExistsSql = DEF_USER_EXISTS_SQL;
    private AuthenticationManager authenticationManager;
    private UserCache userCache = new NullUserCache();

    public CustomizeJdbcUserDetailsManager(DataSource dataSource) {
        setDataSource(dataSource);
    }

    public void setAuthenticationManager(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    public void setUserCache(UserCache userCache) {
        this.userCache = userCache;
    }

    @Override
    public void createUser(UserDetails user) {
        validateUserDetails(user);
        User u = (User) user;
        assert getJdbcTemplate() != null;
        getJdbcTemplate().update(this.createUserSql, (ps) -> {
            ps.setString(1, UUID.randomUUID().toString());
            ps.setString(2, u.getUsername());
            ps.setString(3, u.getPassword());
            ps.setString(4, u.getName());
            ps.setString(5, u.getMobile());
            ps.setString(6, u.getEmail());
            ps.setString(7, u.getHeadImage());
            ps.setDate(8, new java.sql.Date(u.getAccountExpireDate().getTime()));
            ps.setDate(9, new java.sql.Date(u.getCredentialExpireDate().getTime()));
            ps.setBoolean(10, u.isAccountLocked());
            ps.setBoolean(11, user.isEnabled());
        });
        if (getEnableAuthorities()) {
            insertUserAuthorities(user);
        }
    }

    private void insertUserAuthorities(UserDetails user) {
        for (GrantedAuthority auth : user.getAuthorities()) {
            Objects.requireNonNull(getJdbcTemplate()).update(this.createAuthoritySql, user.getUsername(), auth.getAuthority());
        }
    }

    @Override
    public void updateUser(UserDetails user) {
        validateUserDetails(user);
        User u = (User) user;
        Objects.requireNonNull(getJdbcTemplate()).update(this.updateUserSql, (ps) -> {
            ps.setString(1, u.getPassword());
            ps.setString(2, u.getName());
            ps.setString(3, u.getMobile());
            ps.setString(4, u.getEmail());
            ps.setString(5, u.getHeadImage());
            ps.setDate(6, new java.sql.Date(u.getAccountExpireDate().getTime()));
            ps.setDate(7, new java.sql.Date(u.getCredentialExpireDate().getTime()));
            ps.setBoolean(8, u.isAccountLocked());
            ps.setBoolean(9, u.isEnabled());
            ps.setString(10, u.getUsername());
        });
        if (getEnableAuthorities()) {
            deleteUserAuthorities(user.getUsername());
            insertUserAuthorities(user);
        }
        this.userCache.removeUserFromCache(user.getUsername());
    }

    @Override
    public void deleteUser(String username) {

    }

    private void deleteUserAuthorities(String username) {
        Objects.requireNonNull(getJdbcTemplate()).update(this.deleteUserAuthoritiesSql, username);
    }

    @Override
    public boolean userExists(String username) {
        assert getJdbcTemplate() != null;
        List<String> users = getJdbcTemplate().queryForList(this.userExistsSql, String.class, username, username);
        if (users.size() > 1) {
            throw new IncorrectResultSizeDataAccessException("More than one user found with name '" + username + "'",
                    1);
        }
        return users.size() == 1;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        RowMapper<UserDetails> mapper = (rs, rowNum) -> {
            String userName = rs.getString("username");
            String password = rs.getString("password");
            String name = rs.getString("name");
            String mobile = rs.getString("mobile");
            String email = rs.getString("email");
            String headImage = rs.getString("head_image");
            Date accountExpireDate = rs.getDate("account_expire_date");
            Date credentialExpireDate = rs.getDate("credential_expire_date");
            boolean accountLocked = rs.getBoolean("account_locked");
            boolean enabled = rs.getBoolean("enabled");
            return new User(userName, password, name, mobile, email, headImage, accountExpireDate, credentialExpireDate,
                    accountLocked, enabled, AuthorityUtils.NO_AUTHORITIES);
        };
        assert getJdbcTemplate() != null;
        UserDetails user = getJdbcTemplate().queryForObject(getUsersByUsernameQuery(), mapper, username, username);

        if (user == null) {
            this.logger.debug("Query returned no results for user '" + username + "'");
            throw new UsernameNotFoundException(this.messages.getMessage("JdbcDaoImpl.notFound",
                    new Object[]{username}, "Username {0} not found"));
        }
        Set<GrantedAuthority> dbAuthsSet = new HashSet<>();
        if (getEnableAuthorities()) {
            dbAuthsSet.addAll(loadUserAuthorities(user.getUsername()));
        }
        if (this.getEnableGroups()) {
            dbAuthsSet.addAll(loadGroupAuthorities(user.getUsername()));
        }
        List<GrantedAuthority> dbAuths = new ArrayList<>(dbAuthsSet);
        addCustomAuthorities(user.getUsername(), dbAuths);
        if (dbAuths.size() == 0) {
            this.logger.debug("User '" + username + "' has no authorities and will be treated as 'not found'");
            throw new UsernameNotFoundException(this.messages.getMessage("JdbcDaoImpl.noAuthority",
                    new Object[]{username}, "User {0} has no GrantedAuthority"));
        }
        return createUserDetails(username, user, dbAuths);
    }

    protected UserDetails createUserDetails(String username, UserDetails userFromUserQuery,
                                            List<GrantedAuthority> combinedAuthorities) {
        String returnUsername = userFromUserQuery.getUsername();
        if (!this.isUsernameBasedPrimaryKey()) {
            returnUsername = username;
        }
        User user = (User) userFromUserQuery;
        return new User(returnUsername, user.getPassword(), user.getName(), user.getMobile(), user.getEmail(),
                user.getHeadImage(), user.getAccountExpireDate(), user.getCredentialExpireDate(), user.isAccountLocked(),
                user.isEnabled(), combinedAuthorities);
    }

    @Override
    public void changePassword(String oldPassword, String newPassword) throws AuthenticationException {
        Authentication currentUser = SecurityContextHolder.getContext().getAuthentication();
        if (currentUser == null) {
            throw new AccessDeniedException(
                    "Can't change password as no Authentication object found in context " + "for current user.");
        }
        String username = currentUser.getName();
        updatePassword(username, oldPassword, newPassword);
        Authentication authentication = createNewAuthentication(currentUser, newPassword);
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authentication);
        SecurityContextHolder.setContext(context);
    }

    public void resetPassword(ResetPassword resetPassword) {
        updatePassword(resetPassword.getUsername(), resetPassword.getOldPassword(), resetPassword.getNewPassword());
    }

    private void updatePassword(String username, String oldPassword, String newPassword) {
        if (this.authenticationManager != null) {
            this.logger.debug(LogMessage.format("Re Authenticating user '%s' for password change request.", username));
            this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, oldPassword));
        } else {
            this.logger.debug("No authentication manager set. Password won't be re-checked.");
        }
        this.logger.debug("Changing password for user '" + username + "'");
        assert getJdbcTemplate() != null;
        Calendar calendar = new GregorianCalendar();
        calendar.setTime(new Date());
        calendar.add(Calendar.DAY_OF_MONTH, 90);
        getJdbcTemplate().update(this.changePasswordSql, newPassword, calendar.getTime(), username);
        this.userCache.removeUserFromCache(username);
    }

    protected Authentication createNewAuthentication(Authentication currentAuth, String newPassword) {
        UserDetails user = loadUserByUsername(currentAuth.getName());
        UsernamePasswordAuthenticationToken newAuthentication = new UsernamePasswordAuthenticationToken(user, null,
                user.getAuthorities());
        newAuthentication.setDetails(currentAuth.getDetails());
        return newAuthentication;
    }

    private void validateUserDetails(UserDetails user) {
        Assert.hasText(user.getUsername(), "Username may not be empty or null");
        validateAuthorities(user.getAuthorities());
    }

    private void validateAuthorities(Collection<? extends GrantedAuthority> authorities) {
        Assert.notNull(authorities, "Authorities list must not be null");
        for (GrantedAuthority authority : authorities) {
            Assert.notNull(authority, "Authorities list contains a null entry");
            Assert.hasText(authority.getAuthority(), "getAuthority() method must return a non-empty string");
        }
    }

    @Override
    public List<String> findAllGroups() {
        return null;
    }

    @Override
    public List<String> findUsersInGroup(String groupName) {
        return null;
    }

    @Override
    public void createGroup(String groupName, List<GrantedAuthority> authorities) {

    }

    @Override
    public void deleteGroup(String groupName) {

    }

    @Override
    public void renameGroup(String oldName, String newName) {

    }

    @Override
    public void addUserToGroup(String username, String group) {

    }

    @Override
    public void removeUserFromGroup(String username, String groupName) {

    }

    @Override
    public List<GrantedAuthority> findGroupAuthorities(String groupName) {
        return null;
    }

    @Override
    public void addGroupAuthority(String groupName, GrantedAuthority authority) {

    }

    @Override
    public void removeGroupAuthority(String groupName, GrantedAuthority authority) {

    }
}
